package templates;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.MenuBar;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.fixedfunc.GLMatrixFunc;
import javax.media.opengl.glu.GLU;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import util.CalculusUtil;
import util.Camera;
import util.LogUtil;
import util.Parameters;
import util.Util3D;

import com.jogamp.opengl.util.Animator;
import com.jogamp.opengl.util.awt.TextRenderer;

import core.NewOption;
import core.NewOptions;

/**
 * Sert à templater la création d'une fenêtre JOGL.<br>
 * Cette classe doit permettre la création d'une fenêtre de base avec des contrôles de bases. <br>
 * Etendre cette classe suffit à avoir un environnement de création de projet simple déjà configuré/initialisé. <br/>
 * <br>
 * TODO Simplifier/Extraire les parties inutiles
 * 
 * @author sebastien
 * @since 0.0.1
 */
public abstract class JOGLProjectTemplate implements GLEventListener, MouseListener, MouseWheelListener,
		MouseMotionListener, KeyListener {

	// Constants
	/** Logger */
	protected static final Logger LOGGER = LoggerFactory.getLogger(JOGLProjectTemplate.class);

	/** Mode d'affichage par d�faut : en points */
	private static final int DEFAULT_MODE = GL.GL_POINTS;
	/** Delta technique pour la gestion du mouvement de la souris */
	private static final double MOUSE_DELTA = 2.0;
	/** Hauteur par d�faut de la fen�tre */
	private static final int DEFAULT_WINDOW_HEIGTH = 480;
	/** Largeur par d�faut de la fen�tre */
	private static final int DEFAULT_WINDOW_WIDTH = 640;
	/** La distance de recul de la cam�ra */
	private static final int Z_DISTANCE = -50;
	/** Taille de la grille d'affichage */
	private static final int GRID_SCALE = 5;

	private static final int END_LINES = 200;
	private static final int BEGIN_LINES = -END_LINES;

	// Fields
	protected static GL2 gl;
	protected static GLU glu = new GLU();
	protected static GLCanvas canvas = new GLCanvas();
	protected Frame frame;
	// protected JPanel frame;

	protected Animator animator;
	private Dimension dimension;
	protected final Camera<JOGLProjectTemplate> camera;

	// TODO A supprimer => dans Options
	protected boolean animationActivated = false;
	protected NewOptions options;

	protected Parameters parameters;

	private Point startPoint;
	private Point endPoint;

	/** Renderer de texte 2D � l'�cran */
	protected TextRenderer renderer;

	private final CalculusUtil util;

	// Rotations
	protected double rotateX = 0.0f;
	protected double rotateY = 0.0f;

	// Déplacements
	protected double translateX = 0.0;
	protected double translateY = 0.0;
	protected double translateZ = 0.0;
	// Vitesse
	protected double speed = 10.0;

	// Contructor
	public JOGLProjectTemplate() {
		super();
		util = new CalculusUtil();
		options = new NewOptions(true);
		camera = new Camera<JOGLProjectTemplate>(this);
		camera.setZoomDistance(Z_DISTANCE);
	}

	// Methods
	// Interfaces
	// Mouse

	public void mouseClicked(MouseEvent e) {
		LOGGER.debug(LogUtil.logMouseEvents(e));
	}

	public void mouseEntered(MouseEvent e) {}

	public void mouseExited(MouseEvent e) {}

	public void mousePressed(MouseEvent e) {
		// On initialise le point de d�part
		startPoint = new Point(e.getX(), e.getY());
	}

	public void mouseReleased(MouseEvent e) {}

	public void mouseWheelMoved(MouseWheelEvent e) {
		camera.zoom(e.getPreciseWheelRotation());
		LOGGER.debug("Mouse wheel : " + e.getPreciseWheelRotation());
	}

	public void move(double x, double y, double z) {
		translateX = translateX + x;
		translateY = translateY + y;
		translateZ = translateZ + z;
	}

	public void rotate(double x, double y) {
		rotateX = rotateX + x / 10;
		rotateY = rotateY + y / 10;
	}

	public void accelerate(double a) {
		if (a > 0 || (a < 0 && speed > 0)) {
			speed = speed + a;
		}
	}

	public void keyboardGestion(int keyCode) {
		// Axes
		if (keyCode == KeyEvent.VK_A) {
			NewOption.AXES.switchValue();
		}
		// Grille
		if (keyCode == KeyEvent.VK_G) {
			NewOption.GRID.switchValue();
		}
		// Lumières
		if (keyCode == KeyEvent.VK_L) {
			NewOption.LIGHTS.switchValue();
		}
		// Mode d'affichage (points, lignes, polygon...)
		if (keyCode == KeyEvent.VK_M) {
			if (parameters.getMode() == GL.GL_POINTS)
				parameters.setMode(GL.GL_LINES);
			else if (parameters.getMode() == GL.GL_LINES)
				parameters.setMode(GL.GL_LINE_LOOP);
			else if (parameters.getMode() == GL.GL_LINE_LOOP)
				parameters.setMode(GL2.GL_POLYGON);
			else if (parameters.getMode() == GL2.GL_POLYGON)
				parameters.setMode(GL.GL_POINTS);
		}
		// Déplacement
		if (keyCode == KeyEvent.VK_UP) {

		}
	}

	public void mouseDragged(MouseEvent e) {
		endPoint = new Point(e.getX(), e.getY());
		Point delta = new Point();
		delta.setLocation(startPoint.getX() - endPoint.getX(), startPoint.getY() - endPoint.getY());
		double distance = util.distance(startPoint, endPoint);
		if (distance > MOUSE_DELTA) {
			rotate(-delta.getX(), delta.getY());
			LOGGER.trace("Distance : {}", distance);
		}
		startPoint = endPoint;
		LOGGER.trace("Mouse dragged from (" + e.getX() + "," + e.getY() + ")");
	}

	public void mouseMoved(MouseEvent e) {
		LOGGER.trace("Mouse moved to (" + e.getX() + "," + e.getY() + ")");
	}

	// Keyboard

	public void keyPressed(KeyEvent e) {
		LOGGER.trace("Key Pressed : {}", KeyEvent.getKeyText(e.getKeyCode()));
		if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
			exit();
		} else {
			camera.moveCamera(e.getKeyCode());
			keyboardGestion(e.getKeyCode());
		}
	}

	public abstract void resetCamera();

	public void keyReleased(KeyEvent arg0) {}

	public void keyTyped(KeyEvent arg0) {}

	// Init

	public void init(GLAutoDrawable gLDrawable) {
		gl = gLDrawable.getGL().getGL2();
		gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(50.0f, 10.0, 1.0, 1000.0);
		gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl = gLDrawable.getGL().getGL2();

		parameters = new Parameters(gl, DEFAULT_MODE);
		renderer = new TextRenderer(new Font("Console", Font.BOLD, 14));
		((Component) gLDrawable).addKeyListener(this);
		((Component) gLDrawable).addMouseListener(this);
		((Component) gLDrawable).addMouseWheelListener(this);
		((Component) gLDrawable).addMouseMotionListener(this);
	}

	public abstract void enableLights();

	public void display(GLAutoDrawable gLDrawable) {
		initDisplay();

		if ((Boolean) options.getOption("Grille").getValue()) {
			Util3D.draw3DGrid(gl, BEGIN_LINES, END_LINES, GRID_SCALE);
		}
		if ((Boolean) options.getOption("Axes").getValue())
			Util3D.drawAxes(gl, BEGIN_LINES * 2, END_LINES * 2);

		draw();
		if ((Boolean) options.getOption("Animation").getValue()) {
			animate();
		}

	}

	/**
	 * Cette fonction doit gérer tous les aspects d'animation des objets de la fenetre.
	 */
	protected abstract void animate();

	/**
	 * Vide la fenetre et place la caméra
	 */
	private void initDisplay() {
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glLoadIdentity();
		camera.moveForward(gl);

		// rotate on the axis
		gl.glRotated(rotateX, 0.0f, 1.0f, 0.0f);
		gl.glRotated(rotateY, 0.0f, 0.0f, 1.0f);

		gl.glTranslated(translateX, translateY, translateZ);

		// lights
		createLights();
	}

	/**
	 * Cr�e les lumi�res du projet
	 */
	protected abstract void createLights();

	/**
	 * Dessine les objets dans la fen�tre
	 */
	protected abstract void draw();

	public abstract void dispose(GLAutoDrawable arg0);

	public void reshape(GLAutoDrawable gLDrawable, int arg1, int arg2, int width, int height) {
		GL2 gl = gLDrawable.getGL().getGL2();
		if (height <= 0) {
			height = 1;
		}
		float h = (float) width / height;
		gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		gl.glLoadIdentity();
		// TODO Abstraire ?
		glu.gluPerspective(50.0f, h, 1.0, 10000.0);
		//
		gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		gl.glLoadIdentity();
	}

	public void exit() {
		animator.stop();
		frame.dispose();
		System.exit(0);
	}

	// Default call
	public void start(String name) {
		start(name, true, false);
	}

	/**
	 * Init the JOGL window
	 * 
	 * @param isDecorated if frame is decorated (window's limits and buttons)
	 * @param fullScreen if frame should be fullscreen
	 */
	public void start(String name, boolean isDecorated, boolean fullScreen) {
		// frame = new JPanel();
		// GLProfile glp = GLProfile.getDefault();
		// GLCapabilities glCap = new GLCapabilities(glp);
		animator = new Animator(canvas);
		frame = new Frame();
		frame.setName(name);
		frame.setTitle(name);
		frame.setMenuBar(createMenuBar());
		frame.add(canvas);
		frame.setSize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGTH);
		frame.setLocation(0, 0);
		if (dimension != null) {
			frame.setSize(dimension);
		}

		if (fullScreen) {
			// frame.setUndecorated(!isDecorated);
			// frame.setExtendedState(Frame.MAXIMIZED_BOTH);
		}
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				exit();
			}
		});
		frame.setVisible(true);
		LOGGER.info("Window '" + frame.getName() + "' initialized...");
		boolean start = animator.start();
		if (start)
			LOGGER.info("Animator launched...");
		canvas.requestFocus();
		LOGGER.debug("Initialisation Ended");
	}

	public abstract MenuBar createMenuBar();

	// Accessors
	public void setDimension(int width, int height) {
		dimension = new Dimension(width, height);
	}

	public NewOptions getOptions() {
		return options;
	}

	public void setAnimationActivated(boolean animationActivated) {
		this.animationActivated = animationActivated;
	}

	public double getSpeed() {
		return speed;
	}
}
